﻿#region

using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using OMS.Ice.T4Generator.Syntax;

#endregion


namespace OMS.Ice.T4Generator
{
    public class Generator : IGenerator
    {
        private readonly static Dictionary<string, GeneratorInfo> Generators = new Dictionary<string, GeneratorInfo>();

        public Generator()
        {
            Settings = new GeneratorSettings();
        }

        private IGeneratorSettings Settings { get; set; }


        #region IGenerator Members

        IGeneratorSettings IGenerator.Settings
        {
            get { return Settings; }
        }


        void IGenerator.GenerateToFile( string template, string output, params object[] parameters )
        {
            var path = Path.GetDirectoryName( output );
            if(string.IsNullOrEmpty( path ))
                throw new T4Exception(string.Format("Missing output path."));

            try
            {
                if (!Directory.Exists(path))
                    Directory.CreateDirectory( path );

                using( var writer = new StreamWriter( output ) {AutoFlush = true} )
                {
                    Generate( template, writer, parameters );
                    writer.Close();
                }
            }
            catch(T4Exception)
            {
                throw;
            }
            catch( Exception e)
            {
                throw new T4Exception( string.Format("Error while generating template '{0}'. See inner exception.", template), e );
            }
        }


        void IGenerator.Generate( string template, TextWriter output, params object[] parameters )
        {
            try
            {
                Generate( template, output, parameters );
            }
            catch (T4Exception)
            {
                throw;
            }
            catch (Exception e)
            {
                throw new T4Exception(string.Format("Error while generating template '{0}'. See inner exception.", template), e);
            }
        }

        #endregion


        private void Generate( string template, TextWriter output, params object[] parameters )
        {
            GeneratorInfo generatorInfo;
            // Try to get an already compiled generator.
            if( !Generators.TryGetValue( template, out generatorInfo ) )
            {
                // Could not get a generator. Create a new.
                generatorInfo = Compile( template );
                Generators.Add( template, generatorInfo );
            }

            // Get the constructor for the generator,
            var constructor = generatorInfo.GeneratorType.GetConstructor( (from p in parameters
                                                                           select p.GetType()).ToArray() );
// ReSharper disable PossibleNullReferenceException
            var generator = constructor.Invoke( parameters );
// ReSharper restore PossibleNullReferenceException

            // Run the Generate() method.
            generatorInfo.GeneratorType.InvokeMember( "Generate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod, null, generator,
                                                      new object[] {output} );

            // Flush the stream.
            output.Flush();
        }

        private GeneratorInfo Compile( string template )
        {
            // Parse the template
            var parser = new Parser();
            var parserResult = parser.Parse( template, null );

            // Get the language.
// ReSharper disable PossibleMultipleEnumeration
            var templateDirective = (from p in parserResult.Parts
                                     where p is TemplateDirective
                                     select (TemplateDirective)p).First();
// ReSharper restore PossibleMultipleEnumeration

            // Get a code builder.
            var codeBuilder = GetCodeBuilder( templateDirective.Language );

            // Generate the code for the generator.
// ReSharper disable PossibleMultipleEnumeration
            var code = codeBuilder.Generate( parserResult );

            var codeFileName = (from p in parserResult.Parts
                                where p is TemplateDirective
                                select ((TemplateDirective)p).CodeFile).First();

            var codeFile = string.Empty;
            if( !string.IsNullOrEmpty( codeFileName ) )
            {
// ReSharper disable AssignNullToNotNullAttribute
                codeFileName = Path.Combine( Path.GetDirectoryName( template ), codeFileName );
// ReSharper restore AssignNullToNotNullAttribute
                codeFile = File.OpenText( codeFileName ).ReadToEnd();
            }

            var assemlyPaths = GetReferencedAssemlies( parserResult );
// ReSharper restore PossibleMultipleEnumeration

            // Create the generator assembly.
            var provider = GetCodeDomProvider( templateDirective.Language, templateDirective.CompilerVersion );
            var parameter = new CompilerParameters( assemlyPaths)//, codeBuilder.ClassName + ".dll" )
                            {GenerateExecutable = false, GenerateInMemory = true};//, IncludeDebugInformation = true};

            var results = provider.CompileAssemblyFromSource( parameter, new[] {code, codeFile} );
            if (results.Errors.HasErrors)
            {
                throw new T4CompilerException( "Error while building generator assembly.",
                                               (from e in results.Errors.Cast<string>() select e).ToList(),
                                               code, codeFile);
            }

            // Return the generator info.
            var generatorInfo = new GeneratorInfo {Assembly = results.CompiledAssembly};
            generatorInfo.GeneratorType = generatorInfo.Assembly.GetType( codeBuilder.GeneratorType );
            return generatorInfo;
        }

        private string[] GetReferencedAssemlies( ParserResult parserResult )
        {
            // Get all referenced assemblies from the main template.
            var assemblies = (from p in parserResult.Parts
                              where p is AssemblyDirective
                              select ((AssemblyDirective)p).Name).ToList();

            assemblies = assemblies.Union( Settings.ReferenceAssemblies ).ToList();
            // Add all referenced assemblies from the included templates.
            assemblies = assemblies.Union(from i in parserResult.IncludedTemplates.Values
                                  from p in i
                                  where p is AssemblyDirective
                                  select ((AssemblyDirective)p).Name).Distinct().ToList();

            // Include this assembly to the referenced assemblies list.
            assemblies.Insert( 0, Assembly.GetExecutingAssembly().Location );

            var referencePaths = GetReferencePaths();
            var assemlyPaths = assemblies.Select( assembly => FindAssembly( assembly, referencePaths ) ).ToList();
            
            // Add the System.dll as default. Thus it is not needed in the template file.
            assemlyPaths.Add( "System.dll" );

            return assemlyPaths.ToArray();
        }


        private static CodeBuilder GetCodeBuilder( string language )
        {
            CodeBuilder codeBuilder;
            switch( language )
            {
                case "C#":
                    codeBuilder = new CSCodeBuilder();
                    break;
                    //case "VB":
                    //    codeBuilder = new CSCodeBuilder();
                    //    break;
                default:
                    codeBuilder = new CSCodeBuilder();
                    break;
            }

            return codeBuilder;
        }

        private CodeDomProvider GetCodeDomProvider( string language, string compilerVersion )
        {
            if( !CodeDomProvider.IsDefinedLanguage( language ) )
                throw new T4Exception( string.Format( "Unknown language '{0}'.", language ) );

            // Make a copy of the settings.
            var options = new Dictionary<string, string>( Settings.Options );

            if( string.IsNullOrEmpty( compilerVersion) && !options.ContainsKey( "CompilerVersion" ) )
                //options.Add( "CompilerVersion", compilerVersion );
#if !FRAMEWORK35
                options.Add( "CompilerVersion", "v4.0" );
#else
                options.Add( "CompilerVersion", "v3.5" );
#endif


#if !FRAMEWORK35
            return CodeDomProvider.CreateProvider( language, options );
#else
            
            switch( language )
            {
                case "C#":
                    return new CSharpCodeProvider( options );
                case "VB":
                    return new VBCodeProvider( options );
                default:
                    throw new T4Exception( string.Format( "Unknown language '{0}'.", language ) );
            }
#endif
        }


        private IEnumerable<string> GetReferencePaths()
        {
            // Get the path of the generator assembly.
            var result = new List<string> {Path.GetDirectoryName( Assembly.GetExecutingAssembly().Location )};

            result.AddRange( Settings.ReferencePaths );

            return result;
        }


        private string FindAssembly( string fileName, IEnumerable<string> referencePaths )
        {
            // No filename results in null path:
            if( string.IsNullOrEmpty( fileName ) )
                return null;

            if( Path.IsPathRooted( fileName ) )
                return fileName;

            // Search each path directory for the file:
            var path = referencePaths.Select( dir => Path.Combine( dir, fileName ) ).FirstOrDefault( File.Exists );

            if( string.IsNullOrEmpty( path ) )
            {
                // Just return the original filename.
                try
                {
                    if (Settings.UseAssemblyResolver)
                    {
                        // Note: This does only work for application assemblies. 
                        // For assemblies contained in the GAC or for system assemblies we would need the strong name.
                        // ReSharper disable AssignNullToNotNullAttribute
                        var assembly = Assembly.Load( Path.GetFileNameWithoutExtension( Path.GetFileName(fileName) ) );
                        // ReSharper restore AssignNullToNotNullAttribute
                        path = assembly.Location;
                    }
                    else
                    {
                        path = fileName;
                    }
                }
                catch (Exception)
                {
                    path = null;
                }
            }

            return path;
        }


        #region Nested type: GeneratorInfo

        private struct GeneratorInfo
        {
            public Assembly Assembly;
            public Type GeneratorType;
        }

        #endregion


        #region Nested type: GeneratorSettings

        public class GeneratorSettings : IGeneratorSettings
        {
            public GeneratorSettings()
            {
                ReferencePaths = new List<string>();
                ReferenceAssemblies = new List<string>();
                Options = new Dictionary<string, string>();
            }


            #region IGeneratorSettings Members

            public IList<string> ReferencePaths { get; private set; }

            public IList<string> ReferenceAssemblies { get; private set; }

            public IDictionary<string, string> Options { get; private set; }

            public bool UseAssemblyResolver { get; set; }

            #endregion
        }

        #endregion
    }
}